GÄ bortom traditionella exempelbaserade tester. Denna omfattande guide utforskar egenskapbaserad testning i JavaScript med fast-check, vilket hjÀlper dig att hitta fler buggar med mindre kod.
Bortom exempel: En djupdykning i egenskapbaserad testning i JavaScript
Som mjukvaruutvecklare spenderar vi en betydande mÀngd tid pÄ att skriva tester. Vi utformar noggrant enhetstester, integrationstester och end-to-end-tester för att sÀkerstÀlla att vÄra applikationer Àr robusta, pÄlitliga och fria frÄn regressioner. Det dominerande paradigmet för detta Àr exempelbaserad testning. Vi tÀnker pÄ en specifik indata och vi pÄstÄr en specifik utdata. Indata `[1, 2, 3]` ska producera utdata `6`. Indata `"hello"` ska bli `"HELLO"`. Men detta tillvÀgagÄngssÀtt har en tyst, lurande svaghet: vÄr egen fantasi.
TÀnk om du glömmer att testa med en tom array? Ett negativt tal? En strÀng som innehÄller Unicode-tecken? Ett djupt kapslat objekt? Varje missat grÀnsfall Àr en potentiell bugg som vÀntar pÄ att hÀnda. Det Àr hÀr egenskapbaserad testning (PBT) kommer in i bilden och erbjuder ett kraftfullt paradigmskifte som hjÀlper oss att bygga sÀkrare och mer motstÄndskraftig mjukvara.
Denna omfattande guide kommer att guida dig genom vÀrlden av egenskapbaserad testning i JavaScript. Vi kommer att utforska vad det Àr, varför det Àr sÄ effektivt och hur du kan implementera det i dina projekt idag med det populÀra biblioteket `fast-check`.
BegrÀnsningarna med traditionell exempelbaserad testning
LÄt oss övervÀga en enkel funktion som sorterar en array med tal. Med ett populÀrt ramverk som Jest eller Vitest kan vÄrt test se ut sÄ hÀr:
// En enkel (och nÄgot naiv) sorteringsfunktion
function sortNumbers(arr) {
return [...arr].sort((a, b) => a - b);
}
// Ett typiskt exempelbaserat test
test('sortNumbers bör sortera en enkel array korrekt', () => {
const inputArray = [3, 1, 4, 1, 5, 9];
const expectedArray = [1, 1, 3, 4, 5, 9];
expect(sortNumbers(inputArray)).toEqual(expectedArray);
});
Detta test passerar. Vi kan lÀgga till nÄgra fler `it`- eller `test`-block:
- En array som redan Àr sorterad.
- En array med negativa tal.
- En array med noll.
- En tom array.
- En array med dubbletter (vilket vi redan tÀckt).
Vi kÀnner oss bra. Vi har tÀckt grunderna. Men vad har vi missat? Vad sÀgs om `[-0, 0]`? Vad sÀgs om `[Infinity, -Infinity]`? Vad sÀgs om en mycket stor array som kan trÀffa prestandagrÀnser eller konstiga JavaScript-motoroptimeringar? Det grundlÀggande problemet Àr att vi manuellt vÀljer data. VÄra tester Àr bara sÄ bra som de exempel vi kan tÀnka oss, och mÀnniskor Àr notoriskt dÄliga pÄ att förestÀlla sig alla konstiga och underbara sÀtt som data kan struktureras.
Exempelbaserad testning validerar att din kod fungerar för nÄgra handplockade scenarier. Egenskapsbaserad testning validerar att din kod fungerar för hela klasser av indata.
Vad Àr egenskapbaserad testning? Ett paradigmskifte
Egenskapsbaserad testning vÀnder pÄ manuset. IstÀllet för att pÄstÄ att en specifik indata ger en specifik utdata, definierar du en generell egenskap för din kod som ska gÀlla för alla giltiga indata. Testramverket genererar sedan hundratals eller tusentals slumpmÀssiga indata för att försöka bevisa att din egenskap Àr fel.
En "egenskap" Ă€r en invariant â en regel pĂ„ hög nivĂ„ om din funktions beteende. För vĂ„r `sortNumbers`-funktion kan nĂ„gra egenskaper vara:
- Idempotens: Att sortera en redan sorterad array ska inte Àndra den. `sortNumbers(sortNumbers(arr))` ska vara samma som `sortNumbers(arr)`.
- LÀngdinvarians: Den sorterade arrayen ska ha samma lÀngd som den ursprungliga arrayen.
- InnehÄllsinvarians: Den sorterade arrayen ska innehÄlla exakt samma element som den ursprungliga arrayen, bara i en annan ordning.
- Ordning: För alla tvÄ intilliggande element i den sorterade arrayen, `sorted[i] <= sorted[i+1]`.
Detta tillvÀgagÄngssÀtt flyttar dig frÄn att tÀnka pÄ enskilda exempel till att tÀnka pÄ det grundlÀggande kontraktet för din kod. Detta skifte i tankesÀtt Àr otroligt vÀrdefullt för att designa bÀttre, mer förutsÀgbara API:er.
KĂ€rnkomponenterna i PBT
Ett egenskapbaserat testramverk har typiskt tvÄ nyckelkomponenter:
- Generatorer (eller Arbitraries): Dessa ansvarar för att producera ett brett utbud av slumpmÀssiga data enligt specificerade typer (heltal, strÀngar, arrayer av objekt, etc.). De Àr smarta nog att generera inte bara "happy path"-data utan ocksÄ knepiga grÀnsfall som tomma strÀngar, `NaN`, `Infinity` och mer.
- Krympning: Detta Àr den magiska ingrediensen. NÀr ramverket hittar en indata som falsifierar din egenskap (dvs. orsakar ett testfel), rapporterar det inte bara den stora, slumpmÀssiga indatan. IstÀllet försöker det systematiskt hitta den minsta och enklaste indata som fortfarande orsakar felet. Detta gör felsökning exponentiellt enklare.
Komma igÄng: Implementera PBT med `fast-check`
Ăven om det finns flera PBT-bibliotek i JavaScript-ekosystemet Ă€r `fast-check` ett moget, kraftfullt och vĂ€l underhĂ„llet val. Det integreras sömlöst med populĂ€ra testramverk som Jest, Vitest, Mocha och Jasmine.
Installation och installation
Först lÀgger du till `fast-check` i ditt projekts utvecklingsberoenden. Vi antar att du anvÀnder en testkörning som Jest.
npm install --save-dev fast-check jest
# eller
yarn add --dev fast-check jest
# eller
pnpm add -D fast-check jest
Ditt första egenskapbaserade test
LÄt oss skriva om vÄrt `sortNumbers`-test med `fast-check`. Vi kommer att testa "ordning"-egenskapen vi definierade tidigare: varje element ska vara mindre Àn eller lika med det som följer efter det.
import * as fc from 'fast-check';
// Samma funktion som tidigare
function sortNumbers(arr) {
return [...arr].sort((a, b) => a - b);
}
test('utdata frÄn sortNumbers bör vara en sorterad array', () => {
// 1. Beskriv egenskapen
fc.assert(
// 2. Definiera arbitrÀrerna (indatageneratorer)
fc.property(fc.array(fc.integer()), (data) => {
// `data` Àr en slumpmÀssigt genererad array med heltal
const sorted = sortNumbers(data);
// 3. Definiera predikatet (egenskapen som ska kontrolleras)
for (let i = 0; i < sorted.length - 1; ++i) {
if (sorted[i] > sorted[i + 1]) {
return false; // Egenskapen Àr falsifierad
}
}
return true; // Egenskapen gÀller för denna indata
})
);
});
test('sortering bör inte Àndra arraylÀngden', () => {
fc.assert(
fc.property(fc.array(fc.float()), (data) => {
const sorted = sortNumbers(data);
return sorted.length === data.length;
})
);
});
LÄt oss bryta ner detta:
- `fc.assert()`: Detta Àr löparen. Den kommer att köra din egenskapskontroll mÄnga gÄnger (100 som standard).
- `fc.property()`: Detta definierar sjÀlva egenskapen. Den tar en eller flera arbitrarier som argument, följt av en predikatfunktion.
- `fc.array(fc.integer())`: Detta Àr vÄr arbitrÀr. Den instruerar `fast-check` att generera en array (`fc.array`) med heltal (`fc.integer()`). `fast-check` kommer automatiskt att generera arrayer av olika lÀngder, med olika heltalsvÀrden (positiva, negativa, noll, etc.).
- Predikatet: Den anonyma funktionen `(data) => { ... }` Àr dÀr vÄr logik lever. Den fÄr de slumpmÀssigt genererade data och mÄste returnera `true` om egenskapen gÀller eller `false` om den krÀnks. `fast-check` stöder ocksÄ predikatfunktioner som kastar ett fel vid fel, vilket integreras snyggt med Jests `expect`-pÄstÄenden.
Nu, istÀllet för ett test med en handplockad array, har vi ett test som verifierar vÄr sorteringslogik mot 100 olika, automatiskt genererade arrayer varje gÄng vi kör vÄr svit. Vi har massivt ökat vÄr testtÀckning med bara nÄgra rader kod.
Utforska arbitrÀrer: Generera rÀtt data
Kraften i PBT ligger i dess förmÄga att generera mÄngfaldig och utmanande data. `fast-check` tillhandahÄller en rik uppsÀttning arbitrÀrer för att tÀcka nÀstan alla datastrukturer du kan tÀnka dig.
GrundlÀggande arbitrÀrer
Dessa Àr byggstenarna för din datagenerering.
- `fc.integer()`, `fc.float()`, `fc.bigInt()`: För tal. De kan begrÀnsas, t.ex. `fc.integer({ min: 0, max: 100 })`.
- `fc.string()`, `fc.asciiString()`, `fc.unicodeString()`: För strÀngar av olika teckenuppsÀttningar.
- `fc.boolean()`: För `true` eller `false`.
- `fc.constant(value)`: Returnerar alltid samma vÀrde. AnvÀndbart för att blanda med `fc.oneof`.
- `fc.constantFrom(val1, val2, ...)`: Returnerar ett av de angivna konstanta vÀrdena.
Komplexa och sammansatta arbitrÀrer
Du kan kombinera grundlÀggande arbitrarier för att skapa komplexa datastrukturer.
- `fc.array(arbitrary, constraints)`: Genererar en array med element som skapats av den angivna arbitrÀren. Du kan begrÀnsa `minLength` och `maxLength`.
- `fc.tuple(arb1, arb2, ...)`: Genererar en array med fast lÀngd dÀr varje element har en specifik, annorlunda typ.
- `fc.object(shape)`: Genererar objekt med en definierad struktur. Exempel: `fc.object({ id: fc.uuidV(4), name: fc.string() })`.
- `fc.oneof(arb1, arb2, ...)`: Genererar ett vÀrde frÄn nÄgon av de angivna arbitrarierna. Detta Àr utmÀrkt för att testa funktioner som hanterar flera datatyper (t.ex. `string | number`).
- `fc.record({ key: arb, value: arb })`: Genererar objekt som ska anvÀndas som ordböcker eller kartor, dÀr nycklar och vÀrden genereras frÄn arbitrarier.
Skapa anpassade arbitrÀrer med `.map` och `.chain`
Ibland behöver du data som inte passar en standardform. `fast-check` lÄter dig skapa dina egna arbitrÀrer genom att transformera befintliga.
AnvÀnda `.map()`
Metoden `.map()` transformerar utdata frÄn en arbitrÀr till nÄgot annat. LÄt oss till exempel skapa en arbitrÀr som genererar icke-tomma strÀngar.
const nonEmptyStringArb = fc.string({ minLength: 1 });
// Eller, genom att transformera en array med tecken
const nonAStringArb = fc.array(fc.char().filter(c => c !== 'a'))
.map(chars => chars.join(''));
AnvÀnda `.chain()`
Metoden `.chain()` Àr mer kraftfull. Den lÄter dig skapa en ny arbitrÀr baserat pÄ det genererade vÀrdet av en tidigare. Detta Àr viktigt för att skapa korrelerade data.
TÀnk dig att du behöver generera en array och sedan ett giltigt index för samma array. Du kan inte göra detta med tvÄ separata arbitrarier, eftersom indexet kan vara utanför grÀnserna. `.chain()` löser detta perfekt.
// Generera en array och ett giltigt index i den
const arrayAndValidIndexArb = fc.array(fc.anything()).chain(arr => {
// Baserat pÄ den genererade arrayen `arr`, skapa en ny arbitrÀr för indexet
const indexArb = fc.integer({ min: 0, max: arr.length - 1 });
// Returnera en tupel av arrayen och det genererade indexet
return fc.tuple(fc.constant(arr), indexArb);
});
// AnvÀndning i ett test
test('slicing at a valid index should work', () => {
fc.assert(
fc.property(arrayAndValidIndexArb, ([arr, index]) => {
// BÄde `arr` och `index` garanteras vara kompatibla
const sliced = arr.slice(0, index);
expect(sliced.length).toBe(index);
})
);
});
Kraften i krympning: Felsökning gjord enkel
Den enskilt mest övertygande funktionen i egenskapbaserad testning Àr krympning. För att se det i aktion, lÄt oss skapa en avsiktligt buggig funktion.
// Denna funktion misslyckas om indata-arrayen innehÄller talet 42
function sumWithoutBug(arr) {
if (arr.includes(42)) {
throw new Error('Detta nummer Àr inte tillÄtet!');
}
return arr.reduce((acc, val) => acc + val, 0);
}
test('sumWithoutBug ska summera tal', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (data) => {
sumWithoutBug(data);
})
);
});
NÀr du kör detta test kommer `fast-check` nÀstan sÀkert att hitta ett felande fall. Men det kommer inte att rapportera den första slumpmÀssiga arrayen den hittade, som kan vara nÄgot som `[-1024, 500, 42, 987, -2000]`. En felrapport som den dÀr Àr inte sÀrskilt hjÀlpsam. Du skulle behöva inspektera den manuellt för att hitta det problematiska `42`.
IstÀllet kommer `fast-check`s shrinker att sparka in. Den kommer att se felet och börja förenkla indatan:
- Kan jag ta bort ett element? Prova `[500, 42, 987, -2000]`. Misslyckas fortfarande. Bra.
- Kan jag ta bort ett till? Prova `[42, 987, -2000]`. Misslyckas fortfarande.
- ...och sÄ vidare, tills den inte kan ta bort nÄgra fler element utan att fÄ testet att passera.
- Den kommer ocksÄ att försöka göra talen mindre. Kan `42` vara `0`? Nej, testet passerar. Kan det vara `41`? Testet passerar. Det minskar det.
Den slutliga felrapporten kommer att se ut sÄ hÀr:
Error: Property failed after 15 tests
{ seed: 12345678, path: "14", endOnFailure: true }
Counterexample: [[42]]
Shrunk 5 time(s)
Got error: This number is not allowed!
Den talar om för dig exakt, minimalt med indata som orsakade felet: en array som bara innehÄller talet `[42]`. Detta pekar omedelbart pÄ kÀllan till buggen, vilket sparar dig enorm tid och anstrÀngning i felsökning.
Praktiska PBT-strategier och exempel frÄn verkligheten
PBT Àr inte bara för matematiska funktioner. Det Àr ett mÄngsidigt verktyg som kan tillÀmpas pÄ mÄnga omrÄden inom mjukvaruutveckling.
Egenskap: Inversfunktioner
Om du har en funktion som kodar data och en annan som avkodar den, Àr de inverser av varandra. En bra egenskap att testa Àr att avkodning av ett kodat vÀrde alltid ska returnera det ursprungliga vÀrdet.
// `encode` och `decode` kan vara för base64, URI-komponenter eller anpassad serialisering
function encode(obj) { return JSON.stringify(obj); }
function decode(str) { return JSON.parse(str); }
test('decode(encode(x)) ska vara lika med x', () => {
// `fc.jsonValue()` genererar alla giltiga JSON-vÀrden: strÀngar, tal, objekt, arrayer
fc.assert(
fc.property(fc.jsonValue(), (originalValue) => {
const encoded = encode(originalValue);
const decoded = decode(encoded);
expect(decoded).toEqual(originalValue);
})
);
});
Egenskap: Idempotens
En operation Àr idempotent om att tillÀmpa den flera gÄnger har samma effekt som att tillÀmpa den en gÄng. `f(f(x)) === f(x)`. Detta Àr en avgörande egenskap för saker som datastÀdningsfunktioner eller `DELETE`-slutpunkter i ett REST API.
// En funktion som tar bort inledande/efterföljande blanksteg och kollapsar flera mellanslag
function normalizeWhitespace(text) {
return text.trim().replace(/\s+/g, ' ');
}
test('normalizeWhitespace ska vara idempotent', () => {
fc.assert(
fc.property(fc.string(), (originalString) => {
const once = normalizeWhitespace(originalString);
const twice = normalizeWhitespace(once);
expect(twice).toBe(once);
})
);
});
Egenskap: TillstÄndskÀnslig (modellbaserad) testning
Detta Àr en mer avancerad men otroligt kraftfull teknik för att testa system med internt tillstÄnd, som en UI-komponent, en kundvagn eller en tillstÄndsmaskin. Idén Àr att skapa en enkel mjukvarumodell av ditt system och en serie kommandon som kan köras mot bÄde din modell och den verkliga implementeringen. Egenskapen Àr att tillstÄndet i modellen och tillstÄndet i det verkliga systemet alltid ska matcha.
`fast-check` tillhandahÄller `fc.commands` för detta ÀndamÄl. LÄt oss modellera en enkel rÀknare:
// Den verkliga implementeringen
class Counter {
constructor() { this.count = 0; }
increment() { this.count++; }
decrement() { this.count--; }
get() { return this.count; }
}
// Kommandona för fast-check
const incrementCmd = fc.command(
// check: en funktion för att kontrollera om kommandot kan köras pÄ modellen
(model) => true,
// run: en funktion för att köra kommandot pÄ bÄde modellen och det verkliga systemet
(model, real) => {
model.count++;
real.increment();
expect(real.get()).toBe(model.count);
}
);
const decrementCmd = fc.command(
(model) => true,
(model, real) => {
model.count--;
real.decrement();
expect(real.get()).toBe(model.count);
}
);
test('Counter ska bete sig enligt modellen', () => {
fc.assert(
fc.property(fc.commands([incrementCmd, decrementCmd]), (cmds) => {
const model = { count: 0 };
const real = new Counter();
fc.modelRun(() => ({ model, real }), cmds);
})
);
});
I detta test kommer `fast-check` att generera en slumpmÀssig sekvens av `increment`- och `decrement`-kommandon, köra dem mot bÄde vÄr enkla objektmodell och den verkliga `Counter`-klassen och sÀkerstÀlla att de aldrig avviker. Detta kan avslöja subtila buggar i komplex tillstÄndskÀnslig logik som skulle vara nÀstan omöjlig att hitta med exempelbaserad testning.
NÀr du INTE ska anvÀnda egenskapbaserad testning
PBT Àr ett kraftfullt tillÀgg till din testverktygslÄda, men det Àr inte en ersÀttning för alla andra former av testning. Det Àr ingen silverkula.
Exempelbaserad testning Àr ofta bÀttre nÀr:
- Testa specifika, kÀnda affÀrsregler. Om en skatteberÀkning mÄste producera exakt `$10,53` för en specifik indata, Àr ett enkelt exempelbaserat test tydligare och mer direkt. Detta Àr ett regressionstest för ett kÀnt krav.
- "Egenskapen" Àr bara "indata X producerar utdata Y". Om det inte finns nÄgon regel pÄ högre nivÄ, generaliserbar regel om funktionens beteende, kan det vara mer komplext Àn det Àr vÀrt att tvinga fram ett egenskapbaserat test.
- Testar anvĂ€ndargrĂ€nssnitt för visuell korrekthet. Ăven om du kan testa tillstĂ„ndslogiken i en UI-komponent med PBT, hanteras kontroll av en specifik visuell layout eller stil bĂ€st av ögonblicksbildstestning eller verktyg för visuell regression.
Den mest effektiva strategin Àr ett hybridtillvÀgagÄngssÀtt. AnvÀnd egenskapbaserade tester för att stresstesta dina algoritmer, datatransformationer och tillstÄndskÀnslig logik mot ett universum av möjligheter. AnvÀnd traditionella exempelbaserade tester för att spika specifika, kritiska affÀrsbehov och förhindra regressioner pÄ kÀnda buggar.
Slutsats: TĂ€nk i egenskaper, inte bara exempel
Egenskapsbaserad testning uppmuntrar ett djupgÄende skifte i hur vi tÀnker pÄ korrekthet. Det tvingar oss att ta ett steg tillbaka frÄn enskilda exempel och beakta de grundlÀggande principer och kontrakt som vÄr kod bör upprÀtthÄlla. Genom att göra det kan vi:
- UptÀcka överraskande grÀnsfall som vi aldrig hade tÀnkt pÄ att skriva tester för.
- FÄ mycket högre förtroende för robustheten i vÄr kod.
- Skriva mer uttrycksfulla tester som dokumenterar beteendet i vÄrt system snarare Àn bara dess utdata pÄ nÄgra fÄ indata.
- Minskar felsökningstiden drastiskt tack vare kraften i krympning.
Att anta egenskapbaserad testning kan kĂ€nnas obekant till en början, men investeringen Ă€r vĂ€l vĂ€rd det. Börja smĂ„tt. VĂ€lj en ren funktion i din kodbas â en som hanterar datatransformation eller en komplex berĂ€kning â och försök att definiera en egenskap för den. LĂ€gg till ett egenskapbaserat test till ditt nĂ€sta projekt. NĂ€r du bevittnar det hitta sin första icke-triviala bugg, kommer du att vara övertygad om dess kraft att bygga bĂ€ttre, mer tillförlitlig mjukvara för en global publik.
Ytterligare resurser
- fast-check Official Documentation
- FörstÄ egenskapbaserad testning av Scott Wlaschin (en klassisk, sprÄkoppert oberoende introduktion)